package codename.carrot.bomberman.network.server;

import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import android.util.Log;
import codename.carrot.bomberman.game.gamemessages.BombDropMessage;
import codename.carrot.bomberman.game.gamemessages.BombermanMessage;
import codename.carrot.bomberman.game.gamemessages.GameOverMessage;
import codename.carrot.bomberman.game.gamemessages.PlayerItemPickupMessage;
import codename.carrot.bomberman.game.gamemessages.PlayerJoinMessage;
import codename.carrot.bomberman.game.gamemessages.PlayerLostLifeMessage;
import codename.carrot.bomberman.game.gamemessages.PlayerQuitMessage;
import codename.carrot.bomberman.game.networkobjects.DroidPoint;
import codename.carrot.bomberman.models.Level;
import codename.carrot.bomberman.models.Player;
import codename.carrot.bomberman.network.MessageHandler.MessageReceiver;
import codename.carrot.bomberman.network.NetworkConnection;
import codename.carrot.bomberman.network.PostOffice;
import codename.carrot.bomberman.network.events.ReceiveEvent;
import codename.carrot.bomberman.network.interfaces.NetworkMessage;
import codename.carrot.bomberman.network.listener.NetworkServerReceiveListener;

/**
 * The server post office is the main game engine. Its responsibility includes keeping state of the game,
 * alerting clients of changes and maintaining game play.
 * 
 * The server post office is usually not used within the game and does not alert any listeners of changes.
 * Every message processed by the server post office is forwarded to the connected clients.
 * 
 * One of the main changes in sending from that of the Client post office is that the server can send
 * messages independently to a client or exclude a client from a message as well as send to all clients.
 * 
 * @author Group A4.
 *
 */
public class ServerPostOffice extends PostOffice implements NetworkServerReceiveListener{       
	
	/**
	 * The maximum amount of time (in milliseconds) that a client can be unresponsive
	 * in a game.
	 */
	private static final int timeout = 30000; //Timeout in ms.
	/**
	 * An instance of this singleton.
	 */
    private static ServerPostOffice serverPOInstance;
    /**
     * A hash map of ping. The integer as a key is the client id and the value (a long) is the current
     * time in millisecond. Used for keeping track and kick out unresponsive clients.
     */
    private HashMap <Integer, Long> ping = new HashMap<Integer, Long>();
    /**
     * Array of active players.
     */
    private CopyOnWriteArrayList<Player> players = new CopyOnWriteArrayList<Player>();
    /**
     * The current level.
     */
    private Level level;    
    
    
    /**
     * Construct the server post office, privately.
     */
    private ServerPostOffice() {
        NetworkConnection.getInstance().addServerReceiveListener(this);
    }
    
    /**
     * Returns the instance of the server post office that is running. 
     * If the post office is not running, it will be created.
     * @return The server post office.
     */
    public static ServerPostOffice getInstance(){
        if(ServerPostOffice.serverPOInstance == null)
            ServerPostOffice.serverPOInstance = new ServerPostOffice();
        return ServerPostOffice.serverPOInstance;
    }
    
    @Override
    public void destroy() {
        ServerPostOffice.serverPOInstance = null;
    }
    
    /**
     * Sort messages and maintain the game state.
     */
    @Override
    protected void sortMessage(ReceiveEvent e) {
        //SORT MESSAGES HERE.       
        NetworkMessage message = e.getMessage();
        BombermanMessage bomberMessage = null;
        if(message instanceof BombermanMessage) {
        	bomberMessage = (BombermanMessage) message;
        	if(!(bomberMessage instanceof PlayerJoinMessage))
        		ping(bomberMessage.getPlayerId());
        }
        if(bomberMessage == null) return; // We don't want to touch this message            
        
        if(bomberMessage instanceof PlayerJoinMessage) {
            PlayerJoinMessage pm = (PlayerJoinMessage) bomberMessage;
            int playerId = bomberMessage.getPlayerId();
            if(playerId != -1) return;         
            //Check if player already exist & remove.
            Player remove = null;
            for (Player p: players){
            	if(p.getPlayerId() == e.getClientid())
            		remove = p;
            }
            if(remove != null)
            	players.remove(remove);
            
            playerId = e.getClientid();
            pm.setPlayerId(playerId);
            // Time to send the join-message back to the client, with the playerID (client ID)
            pm.setConcernsReceiver(true); 
            sendMessage(pm, playerId, 0, MessageReceiver.INDEPENDENT);
            
            PlayerJoinMessage pmNew = (PlayerJoinMessage)pm.clone();
            pmNew.setConcernsReceiver(false);
            // Also, send all the existing players back to the newly connected client
            for(Player p : players) {
                PlayerJoinMessage pmExistingPlayer = new PlayerJoinMessage(p.getPlayerId(), p.getNickname(), false);
                this.sendMessage(pmExistingPlayer, playerId, 0, MessageReceiver.INDEPENDENT);
            }           
            players.add(new Player(0, 0, playerId, pmNew.getPlayerNickname()));
            
            // Send the newly connected client to all other connected clients..
            pmNew.setConcernsReceiver(false);
            this.sendMessage(pmNew, pmNew.getPlayerId(), 0, MessageReceiver.EXCLUSIVE);
        }       
        else if(bomberMessage instanceof PlayerItemPickupMessage) {
            PlayerItemPickupMessage pm = (PlayerItemPickupMessage) message;
            // Let's see if the item is available on mah map, otherwise.. DECLINE! lol
            int xCoor = pm.getPosition().x;
            int yCoor = pm.getPosition().y;                             
            if(level.getItem(xCoor, yCoor) != null) {
                // We're only accepting this pickup if the item is available      
                this.sendMessage(bomberMessage, 0);
            }
        }       
        else if(bomberMessage instanceof BombDropMessage) {
            BombDropMessage bm = (BombDropMessage) message;
            DroidPoint bombPos = bm.getPosition();
            // Check wether or not there's a bomb on this location already
            if(level.getObstacle(bombPos.x, bombPos.y) == null) {
                // We'll forward this message as there's no bomb OR other obstacle
                // in this location
                 this.sendMessage(bomberMessage, 0);                           
            }                
        }
        else if(bomberMessage instanceof PlayerQuitMessage) {
            // Forward the message
             this.sendMessage(bomberMessage, 0);
               // Also, terminate the connection to this specific client
            Log.d(DEBUG, "Trying to terminate client connection");
            Player remove = null;
            for(Player p: players){
                if(p.getPlayerId() == bomberMessage.getPlayerId()){
                    remove = p;
                    break;
                }
            }
            for(Player p : players) {
                Log.d("SERVERTEST", "Player: " + p.getNickname() + " ID: " + p.getPlayerId());
            }
            players.remove(remove);
        }
        else if (bomberMessage instanceof PlayerLostLifeMessage){
        	//DISTRIBUTE TO CLIENTS FIRST!
        	this.sendMessage(bomberMessage, 0);
        	//SEND GAME OVER MESSAGE IF ALL EXCEPT ONE IS DEAD.
        	PlayerLostLifeMessage pllm = (PlayerLostLifeMessage) bomberMessage;
        	if(pllm.hasDied()){
        		for(Player p: players){
        			if(p.getPlayerId() == pllm.getPlayerId()){
        				p.setIsDead(true);
        				ping.remove(p.getPlayerId());
        			}
        		}
        		int numOfDead = 0;
        		for(Player p: players){
        			if(p.isDead())
        				numOfDead++;
        		}
        		if(numOfDead == players.size()-1){
            		for(Player p: players){
            			if(!p.isDead()){
                    		GameOverMessage newGom = new GameOverMessage();
                    		newGom.setWinner(p.getPlayerId());
                    		this.sendMessage(newGom, 0);
                    		break;
            			}
            		}

        		}
        	}
        } else {
            // Fuck it, let's distribute this motherfucker to all of mah clients 
             this.sendMessage(bomberMessage, 0);
        }
    }

    /**
     * Set the current level.
     * @param level The current level.
     */
    public void setLevel(Level level) {
        this.level = level;
    }
    
    /**
     * Store and keep track of time when messages arrive.
     * 
     * Unresponsive clients will be disconnected.
     * 
     * This is only used midt-game, not while in game lobby (gameInitMessage) and not if player is dead.
     * 
     * @param id The id of the client.
     */
    private void ping(int id){
    	ping.put(id, System.currentTimeMillis());
    	for (int i: ping.keySet()){
    		long time = ping.get(i);
    		if(time+timeout < System.currentTimeMillis()){
    			Log.d(DEBUG, "TIMEOUT - client: " + i);
                Player remove = null;
                for (Player p: players){
                	if(p.getPlayerId() == i && !p.isDead())
                		remove = p;
                }
                if(remove != null){
                	players.remove(remove);
                	NetworkConnection.getInstance().getServerConnection().terminateClientConnection(i);
                }
    		}
    	}    		
    }

    @Override
    public void onMessageReceive(ReceiveEvent e) {
    	Log.d(DEBUG, "onMessageReceive FROM " + e.getClientid());
        sortMessage(e);
    }
    
    @Override
    public void sendMessage(BombermanMessage message, int clientFrom) {
        NetworkConnection.getInstance().sendMessage(message, clientFrom);
    }
    
    /**
     * Send a message to a specific client or exluding a spesific client.
     * @param message The message to send.
     * @param clientTo The receiving client.
     * @param clientFrom The sending client (0 for server).
     * @param messageReceiver The type of message (INDEPENDENT, EXCLUSIVE or INCLUSIVE):
     */
    private void sendMessage(BombermanMessage message, int clientTo, int clientFrom, MessageReceiver messageReceiver) {
        NetworkConnection.getInstance().sendMessage(message, clientTo, clientFrom, messageReceiver);
    }

	@Override
	protected void sendMessage(BombermanMessage message) {}

}


